/*
 * Copyright 2009 Tran Nhut Trung.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */
package com.gdevelop.gwtxp.gen;


import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.HasDirection;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.MenuItemSeparator;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.TextBoxBase;
import com.google.gwt.user.client.ui.Tree;
import com.google.gwt.user.client.ui.TreeItem;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;


public class SchemaGenerator {
  private static enum HorizontalAlignmentConstant{
    ALIGN_DEFAULT, ALIGN_CENTER, ALIGN_LEFT, ALIGN_RIGHT
  }
  private static enum TextAlignConstant{
    ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, ALIGN_JUSTIFY
  }
  private static enum VerticalAlignmentConstant{
    ALIGN_TOP, ALIGN_MIDDLE, ALIGN_BOTTOM
  }

  private static final String NAMESPACE_URI = "http://www.gdevelop.com/gwtxp";
  
  private static final List generatedSchemaTypes = new ArrayList();
  private static final List generatedSchemaElements = new ArrayList();
  private static final List generatedSchemaEnumTypes = new ArrayList();
  
  public static void main(String[] args) throws Exception{
    args = new String[0];
    
    generateSchema();
  }

  private static void generateSchema() throws Exception {
    File f = new File("gen-src");
    f.mkdir();
    f = new File("gen-src/gwtXP.xsd");

    OutputStream os = new FileOutputStream(f);
    CodePrintWriter out = new CodePrintWriter(os);
    
    out.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
    out.println("<xsd:schema version=\"1.0\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"");
    out.indent();
    out.println("xmlns=\"" + NAMESPACE_URI + "\"");
    out.println("targetNamespace=\"" + NAMESPACE_URI + "\"");
    out.println("elementFormDefault=\"qualified\">");
    out.println();
    
    generateSchemaEnumType(out, HasDirection.Direction.class);
    out.println();
    generateSchemaEnumType(out, VerticalAlignmentConstant.class);
    out.println();
    generateSchemaEnumType(out, HorizontalAlignmentConstant.class);
    out.println();
    generateSchemaEnumType(out, TextAlignConstant.class);
    out.println();
    
    for (Class clazz : UIModels.MODEL_CLASSES){
      generateSchemaType(out, clazz);
    }
    
    for (Class clazz : UIModels.MODEL_CLASSES){
      generateSchemaElement(out, clazz);
    }
    
    out.println();
    out.println("<xsd:element name=\"uiObject\" type=\"UIObjectType\" abstract=\"true\"/>");
    out.println("<xsd:element name=\"widget\" type=\"WidgetType\" abstract=\"true\"/>");
    out.println("<xsd:complexType name=\"gwtxpType\">");
    out.indent();
    out.println("<xsd:sequence>");
    out.indent();
    out.println("<xsd:element ref=\"widget\" maxOccurs=\"1\"/>");
    out.outdent();
    out.println("</xsd:sequence>");
    out.println("<xsd:attribute name=\"controllerClassName\" type=\"xsd:string\"/>");
    out.outdent();
    out.println("</xsd:complexType>");
    out.println("<xsd:element name=\"gwtxp\" type=\"gwtxpType\"/>");

    out.outdent();
    out.println("</xsd:schema>");
    
    out.println("<!-- TODO:");
    out.println("xsd:restriction for SimplePanelType");
    out.println("-->");
    out.flush();
    os.close();
  }

  private static void generateSchemaType(CodePrintWriter out, Class clazz) throws Exception {
    if (!UIObject.class.isAssignableFrom(clazz)){
      return;
    }
    if (generatedSchemaTypes.contains(clazz)){
      return;
    }
    generateSchemaType(out, clazz.getSuperclass());
    
    boolean isAbstract = Modifier.isAbstract(clazz.getModifiers());
    String superClass = clazz == UIObject.class ? null : clazz.getSuperclass().getSimpleName() + "Type";
    
    // String clazzName = clazz.getSimpleName();
    String clazzName = toXSDTypeName(clazz);
    out.println("<xsd:complexType name=\"" + clazzName + "Type\"" + 
                (isAbstract ? " abstract=\"true\"" : "") + ">");
    out.indent();

    if (superClass != null){
      out.println("<xsd:complexContent>");
      out.indent();
      out.println("<xsd:extension base=\"" + superClass + "\">");
      out.indent();
      
      if (MenuBar.class == clazz){
        out.println("<xsd:choice minOccurs=\"0\" maxOccurs=\"unbounded\">");
        out.indent();
        out.println("<xsd:element ref=\"menuItem\"/>");
        out.println("<xsd:element ref=\"menuSeparator\"/>");
        out.outdent();
        out.println("</xsd:choice>");
      }else if (MenuItem.class == clazz){
        out.println("<xsd:sequence>");
        out.indent();
        out.println("<xsd:element ref=\"menuBar\" minOccurs=\"0\" maxOccurs=\"1\"/>");
        out.outdent();
        out.println("</xsd:sequence>");
      }else if (Tree.class == clazz){
        out.println("<xsd:sequence>");
        out.indent();
        out.println("<xsd:element ref=\"treeItem\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>");
        out.outdent();
        out.println("</xsd:sequence>");
      }else if (TreeItem.class == clazz){
        out.println("<xsd:sequence>");
        out.indent();
        out.println("<xsd:element ref=\"treeItem\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>");
        out.outdent();
        out.println("</xsd:sequence>");
      }else if (
        // Panel
        (Panel.class == clazz)
        // not panel but HasWidgets (e.g extends Composite implements HasWidgets)
        || (!Panel.class.isAssignableFrom(clazz) && HasWidgets.class.isAssignableFrom(clazz))
      ){
        out.println("<xsd:sequence>");
        out.indent();
        out.println("<xsd:element ref=\"widget\" minOccurs=\"0\" maxOccurs=\"unbounded\"/>");
        out.outdent();
        out.println("</xsd:sequence>");
      }
    }
    BeanInfo beanInfo = Introspector.getBeanInfo(clazz, clazz.getSuperclass());
    PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
    // attribute
    for (PropertyDescriptor propertyDescriptor : propertyDescriptors){
      // if ((propertyDescriptor.getReadMethod() != null) && 
      //    (propertyDescriptor.getWriteMethod() != null)){
      if (propertyDescriptor.getWriteMethod() != null){
        String propName = propertyDescriptor.getName();
        Class propType = propertyDescriptor.getPropertyType();
        if (propType == Widget.class){
          continue;
        }
        
        // Exception cases
        if ((clazz == CheckBox.class) && ("value".equals(propName))){
          propType = boolean.class;
        }
        if ((clazz == MenuItem.class) && ("command".equals(propName))){
          out.println("<xsd:attribute name=\"onCommand\" type=\"xsd:string\"/>");
        }
        
        if (isOverwritingMethod(clazz, propertyDescriptor.getWriteMethod())){
          continue;
        }
        
        out.println("<xsd:attribute name=\"" + propName + "\" type=\"" + toXSDType(propType) + "\"/>");
      }
    }
    
    // Handlers
    for (Method m : clazz.getMethods()){
      if (isOverwritingMethod(clazz, m)){
        continue;
      }

      String methodName = m.getName();
      if ((methodName.startsWith("add")) 
          && (methodName.endsWith("Handler")) 
          && (m.getReturnType() == HandlerRegistration.class)){
        String handlerName = methodName.substring(3, methodName.length()-7);
        out.println("<xsd:attribute name=\"on" + handlerName + "\" type=\"xsd:string\"/>");
      }
    }
    
    // Additional attribues
    List<UIModels.ATTRIBUTE> attrs = UIModels.ADDITIONAL_ATTRS.get(clazz);
    if (attrs != null){
      for (UIModels.ATTRIBUTE attr : attrs){
        out.println("<xsd:attribute name=\"" + attr.name + "\" type=\"" + toXSDType(attr.type) + "\"/>");
      }
    }
    
    if (superClass != null){
      out.outdent();
      out.println("</xsd:extension>");
      out.outdent();
      out.println("</xsd:complexContent>");
    }

    out.outdent();
    out.println("</xsd:complexType>");
    out.println();
    
    generatedSchemaTypes.add(clazz);
  }
  
  private static void generateSchemaElement(CodePrintWriter out, Class clazz) throws Exception {
    if (!UIObject.class.isAssignableFrom(clazz)){
      return;
    }
    if (generatedSchemaElements.contains(clazz)){
      return;
    }
    generateSchemaElement(out, clazz.getSuperclass());
    
    // String clazzName = clazz.getSimpleName();
    String clazzName = toXSDTypeName(clazz);
    boolean isAbstract = Modifier.isAbstract(clazz.getModifiers());
    if ((Panel.class == clazz) || (Widget.class == clazz)){
      isAbstract = true;
    }
    if (!isAbstract){
      out.println("<xsd:element name=\"" + 
          clazzName.substring(0, 1).toLowerCase() + clazzName.substring(1) +
          "\" type=\"" + clazzName + "Type\"" + 
          " substitutionGroup=\"" + (Widget.class.isAssignableFrom(clazz) ? "widget" : "uiObject") +
          "\"/>"); 
    }
    
    generatedSchemaElements.add(clazz);
  }
  
  private static void generateSchemaEnumType(CodePrintWriter out, Class clazz) throws Exception {
    if (!clazz.isEnum()){
      return;
    }
    if (generatedSchemaEnumTypes.contains(clazz)){
      return;
    }
    
    out.println("<xsd:simpleType name=\"" + clazz.getSimpleName() + "Type\">");
    out.indent();
    out.println("<xsd:restriction base=\"xsd:string\">");
    out.indent();
    for (Object o : clazz.getEnumConstants()){
      out.println("<xsd:enumeration value=\"" + o + "\"/>");
    }
    out.outdent();
    out.println("</xsd:restriction>");
    out.outdent();
    out.println("</xsd:simpleType>");
    
    generatedSchemaEnumTypes.add(clazz);
  }
  
  private static String toXSDType(Class type){
    // TODO: type is of string for ${bean.prop}
    if (true) return "xsd:string";
    
    if (type == String.class){
      return "xsd:string";
    }
    if (type == Date.class){
      return "xsd:dateTime";
    }
    if (type == java.sql.Date.class){
      return "xsd:dateTime";
    }
    
    if ((type == Double.class) || (type == double.class)){
      return "xsd:decimal";
    }
    if ((type == Float.class) || (type == float.class)){
      return "xsd:decimal";
    }
    if ((type == Long.class) || (type == long.class)){
      return "xsd:long";
    }
    if ((type == Integer.class) || (type == int.class)){
      return "xsd:int";
    }
    if ((type == Short.class) || (type == short.class)){
      return "xsd:short";
    }
    if ((type == Byte.class) || (type == byte.class)){
      return "xsd:byte";
    }
    if ((type == Character.class) || (type == char.class)){
      return "xsd:string";
    }
    if ((type == Boolean.class) || (type == boolean.class)){
      return "xsd:boolean";
    }
    
    if (type.isEnum()){
      return type.getSimpleName() + "Type";
    }
    
    if (type == HasHorizontalAlignment.HorizontalAlignmentConstant.class){
      return HorizontalAlignmentConstant.class.getSimpleName() + "Type";
    }
    if (type == HasVerticalAlignment.VerticalAlignmentConstant.class){
      return VerticalAlignmentConstant.class.getSimpleName() + "Type";
    }
    if (type == TextBoxBase.TextAlignConstant.class){
      return TextAlignConstant.class.getSimpleName() + "Type";
    }
    
    System.out.println("No xsd type for " + type.getName());
    
    return "any";
  }
  
  private static String toXSDTypeName(Class type){
    if (type == HTML.class){
      return "html";
    }
    if (type == Hyperlink.class){
      return "hyperlink";
    }
    if (type == MenuItemSeparator.class){
      return "menuSeparator";
    }
    
    return type.getSimpleName();
  }
  
  /**
   * Check whether method m is an overwriting in class clazz
   * @param clazz
   * @param m
   * @return
   */
  private static boolean isOverwritingMethod(Class clazz, Method m){
    String methodName = m.getName();
    Class[] paramTypes = m.getParameterTypes();
    try{
      if (clazz.getSuperclass().getMethod(methodName, paramTypes) != null){
        return true;
      }
    }catch(NoSuchMethodException e){
      return false;
    }
    return false;
  }
}
